package org.xx.armory.spring5.mvc;

import org.slf4j.LoggerFactory;
import org.springframework.boot.web.client.RestTemplateCustomizer;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.FormHttpMessageConverter;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriTemplateHandler;

import javax.net.ssl.HttpsURLConnection;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URLDecoder;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

import static java.util.Arrays.stream;
import static org.slf4j.LoggerFactory.getLogger;
import static org.springframework.http.HttpMethod.DELETE;
import static org.springframework.http.HttpMethod.GET;
import static org.springframework.http.HttpMethod.PATCH;
import static org.springframework.http.HttpMethod.POST;
import static org.springframework.http.HttpMethod.PUT;
import static org.springframework.http.HttpStatus.OK;
import static org.springframework.web.util.UriComponentsBuilder.fromHttpUrl;
import static org.xx.armory.commons.DebugUtils.dumpTraceBack;
import static org.xx.armory.commons.Validators.rejectIfBlank;
import static org.xx.armory.commons.Validators.rejectIfNull;
import static org.xx.armory.httpclient.SSLContextUtils.allowAllHostname;
import static org.xx.armory.httpclient.SSLContextUtils.ignoreCert;

/**
 * 异常安全的Rest模板实现。
 */
public final class RestTemplateUtils
        extends RestTemplate {
    private RestTemplateUtils() {
        throw new AssertionError();
    }

    /**
     * 创建一个表单数据构造器。
     *
     * @return 表单数据构造器。
     */
    public static FormDataBuilder formDataBuilder() {
        return new FormDataBuilder();
    }

//    public static ClientHttpRequestInterceptor loggingInterceptor() {
//        return new LoggingInterceptor_();
//    }
//
//    private static class LoggingInterceptor_
//            implements ClientHttpRequestInterceptor {
//        private final Logger logger = LoggerFactory.getLogger(RestTemplateUtils.class);
//
//        private final int maxBodyLength;
//
//        public LoggingInterceptor_() {
//            this(4096);
//        }
//
//        public LoggingInterceptor_(
//                int maxBodyLength
//        ) {
//            this.maxBodyLength = maxBodyLength;
//        }
//
//        @Override
//        public ClientHttpResponse intercept(
//                HttpRequest request,
//                byte[] body,
//                ClientHttpRequestExecution execution
//        )
//                throws IOException {
//            logRequest(request, body);
//            final var response = execution.execute(request, body);
//            logResponse(request, response);
//
//            return response;
//        }
//
//        private void appendBody(
//                StringBuilder buffer,
//                byte[] body,
//                Charset charset
//        ) {
//            if (charset == null) {
//                charset = UTF_8;
//            }
//        }
//
//        private void logRequest(
//                HttpRequest request,
//                byte[] body
//        ) {
//            if (logger.isDebugEnabled()) {
//                final var msg = new StringBuilder();
//                msg.append("Send data to:\n")
//                   .append("[").append(request.getMethod())
//                   .append(request.getURI()).append("]\n");
//
//                if (body != null && body.length > 0) {
//                    msg.append(body.length).append("byte(s)\n");
//
//                    final var contentType = request.getHeaders().getContentType();
//                    if (contentType != null && (
//                            contentType.isCompatibleWith(MimeTypeUtils.TEXT_PLAIN)
//                                    || contentType.isCompatibleWith(MimeTypeUtils.APPLICATION_JSON)
//                                    || contentType.isCompatibleWith(MimeTypeUtils.APPLICATION_XML))
//                    ) {
//                        appendBody(msg, body, contentType.getCharset());
//                    }
//                }
//                logger.debug(msg.toString());
//            }
//        }
//
//        private void logResponse(
//                HttpRequest request,
//                ClientHttpResponse response
//        ) {
//            if (logger.isDebugEnabled()) {
//                final var msg = new StringBuilder();
//                msg.append("Received data from:\n")
//                   .append("[").append(request.getMethod())
//                   .append(request.getURI()).append("]\n");
//
//                final var body = response.getBody();
//
//                if (body != null && body.length > 0) {
//                    msg.append(body.length).append("byte(s)\n");
//
//                    final var contentType = request.getHeaders().getContentType();
//                    if (contentType != null && (
//                            contentType.isCompatibleWith(MimeTypeUtils.TEXT_PLAIN)
//                                    || contentType.isCompatibleWith(MimeTypeUtils.APPLICATION_JSON)
//                                    || contentType.isCompatibleWith(MimeTypeUtils.APPLICATION_XML))
//                    ) {
//                        appendBody(msg, body, contentType.getCharset());
//                    }
//                }
//                logger.debug(msg.toString());
//            }
//        }
//    }

    /**
     * 创建自定义类，用于忽略HTTPS连接中的证书。
     *
     * @return 自定义类。
     */
    public static RestTemplateCustomizer ignoreCertCustomizer() {
        return restTemplate -> restTemplate.setRequestFactory(new IgnoreCertClientHttpRequestFactory());
    }

    public static Map<String, String> parseEncodedQueryString(
            String encodedQueryString,
            Charset charset
    ) {
        return stream(encodedQueryString.split("&"))
                .map(com -> {
                    final var ep = com.indexOf('=');
                    if (ep < 0) {
                        return null;
                    }

                    final var key = URLDecoder.decode(com.substring(0, ep).strip(), charset);
                    final var value = URLDecoder.decode(com.substring(ep + 1).strip(), charset);

                    return new String[]{key, value};
                }).filter(Objects::nonNull)
                .collect(Collectors.toMap(kv -> kv[0], kv -> kv[1]));
    }

    /**
     * 通过REST模板类，调用其它服务。
     *
     * <p>如果调用失败，那么记录日志，并返回 {@code null}。</p>
     * <p>{@link RestTemplate}类型接收 {@link MultiValueMap}类型的数据作为 {@code formData} 时，
     * 自动使用 {@literal application/www-form-urlencoded} 或者 {@literal multipart/form-data}方式编码，
     * 默认字符集是 {@link FormHttpMessageConverter#DEFAULT_CHARSET}。</p>
     *
     * @param restTemplate
     *         用于调用其它服务的REST模板类。
     * @param url
     *         服务的URL。
     * @param method
     *         调用方法。
     * @param params
     *         URL中的参数，{@code null}表示不使用参数。
     * @param formData
     *         通过FORM表单传递的参数，{@code null}表示不使用参数。
     * @param resultClass
     *         期望的返回结果类型。
     * @param <T>
     *         期望的返回结果类型。
     * @return 返回结果。
     * @throws IllegalArgumentException
     *         如果参数 {@code restTemplate}是 {@code null}，或者 {@code resultClass}是 {@code null}，
     *         或者参数 {@code url} 是 {@code null}或者只包含空白字符。
     */
    public static <T> T exchange(
            RestTemplate restTemplate,
            String url,
            HttpMethod method,
            Map<String, ?> params,
            Map<String, ?> formData,
            Class<T> resultClass
    ) {
        rejectIfNull(restTemplate, "restTemplate");
        rejectIfNull(resultClass, "resultClass");
        url = rejectIfBlank(url, "url").strip();

        restTemplate.setUriTemplateHandler(new UriTemplateHandler() {
            @Override
            public URI expand(
                    String uriTemplate,
                    Map<String, ?> uriVariables
            ) {
                return null;
            }

            @Override
            public URI expand(
                    String uriTemplate,
                    Object... uriVariables
            ) {
                return null;
            }
        });
        final URI url_;
        if (params != null && !params.isEmpty()) {
            final var params_ = new LinkedMultiValueMap<String, String>(params.size());
            params.forEach((k, v) -> {
                if (k != null && !k.isEmpty() && v != null) {
                    final var sv = String.valueOf(v);
                    if (!sv.isEmpty()) {
                        params_.add(k, sv);
                    }
                }
            });
            url_ = fromHttpUrl(url).queryParams(params_).build().toUri();
        } else {
            url_ = fromHttpUrl(url).build().toUri();
        }

        final HttpEntity<?> requestEntity;
        if (formData != null && !formData.isEmpty() &&
                (method == POST || method == PUT || method == PATCH)) {
            if (formData instanceof MultiValueMap) {
                requestEntity = new HttpEntity<>((MultiValueMap<?, ?>) formData);
            } else {
                final var formData_ = new LinkedMultiValueMap<String, Object>(formData.size());
                formData.entrySet()
                        .stream()
                        .filter(entry -> entry.getKey() != null && !entry.getKey().isBlank() && entry.getValue() != null)
                        .forEach(entry -> formData_.add(entry.getKey(), entry.getValue()));
                requestEntity = new HttpEntity<>(formData_);
            }
        } else {
            requestEntity = null;
        }

        try {
            final var rsp = restTemplate.exchange(url_, method, requestEntity, resultClass);
            if (rsp.getStatusCode() == OK) {
                return rsp.getBody();
            } else {
                getLogger(RestTemplateUtils.class).warn("Cannot exchange data with: {} {}, status code = {}", method, url_, rsp.getStatusCodeValue());
                return null;
            }
        } catch (RestClientException ex) {
            getLogger(RestTemplateUtils.class).warn("Cannot exchange data with: {} {}\n{}", method, url_, dumpTraceBack(ex));
            return null;
        }
    }

    public static <T> T getForObject(
            RestTemplate restTemplate,
            String url,
            Map<String, ?> params,
            Class<T> resultClass
    ) {
        return exchange(restTemplate, url, GET, params, null, resultClass);
    }

    public static <T> T postForObject(
            RestTemplate restTemplate,
            String url,
            Map<String, ?> params,
            MultiValueMap<String, ?> formData,
            Class<T> resultClass
    ) {
        return exchange(restTemplate, url, POST, params, formData, resultClass);
    }

    public static void delete(
            RestTemplate restTemplate,
            String url,
            Map<String, ?> params
    ) {
        exchange(restTemplate, url, DELETE, params, null, Void.class);
    }

    private static class IgnoreCertClientHttpRequestFactory
            extends SimpleClientHttpRequestFactory {
        @Override
        protected void prepareConnection(
                HttpURLConnection connection,
                String httpMethod
        )
                throws IOException {
            if (connection instanceof HttpsURLConnection) {
                LoggerFactory.getLogger(RestTemplateUtils.class).debug("Prepare https connection {}", connection);
                ((HttpsURLConnection) connection).setHostnameVerifier(allowAllHostname());
                ((HttpsURLConnection) connection).setSSLSocketFactory(ignoreCert().getSocketFactory());
            } else {
                LoggerFactory.getLogger(RestTemplateUtils.class).debug("Prepare http connection {}", connection);
            }

            super.prepareConnection(connection, httpMethod);
        }
    }

    public static class FormDataBuilder {
        private final MultiValueMap<String, Object> result;

        protected FormDataBuilder() {
            this.result = new LinkedMultiValueMap<>();
        }

        public FormDataBuilder add(
                String key,
                Object value
        ) {
            this.result.add(key, value);
            return this;
        }

        public FormDataBuilder addAll(
                Map<String, ?> src
        ) {
            if (src != null) {
                src.forEach(this.result::add);
            }

            return this;
        }

        public MultiValueMap<String, Object> build() {
            return new LinkedMultiValueMap<>(this.result);
        }

        public Map<String, Object> buildHash() {
            return new HashMap<>(this.result.toSingleValueMap());
        }
    }
}
